package com.Sdebruijn.curriculumvitae.sync.remote;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Vector;

import com.Sdebruijn.curriculumvitae.sync.local.BaseKeys;
import com.Sdebruijn.curriculumvitae.sync.local.DirNotCreatedException;
import com.Sdebruijn.curriculumvitae.sync.local.ImageMaxSize;
import com.Sdebruijn.curriculumvitae.sync.local.Job;

import roboguice.util.Ln;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.media.ThumbnailUtils;
import android.provider.MediaStore;

/**
 * Class that performs the download in a background thread and notify the UI
 * with the changes. A BroadcastReceiver can be used to inform the user that
 * some background work are still in progress Use:
 * {@link BaseKeys.BACKGROUND_DATA_FILTER} as Intent action and
 * {@link BaseKeys.IS_DOWNLOADING} to get background thread status
 * 
 */
public final class DownloadTask {

	/** The s instance. */
	private static volatile DownloadTask sInstance = null;

	/** The download thread. */
	private final Thread downloadThread;

	/** The need refresh. */
	private volatile boolean needRefresh = true;

	/** The force stop. */
	private volatile boolean forceStop = false;

	/** The lock thread. */
	private final Object lockThread;

	/** The jobs high. */
	private final Vector<Job> jobsLow, jobsHigh;

	/** The context. */
	private Context context;

	/** The intent. */
	private final Intent intent;

	/** The buffer size used for download. */
	private static final int BUFFER_SIZE = 4096;

	/** File output buffer size. */
	private static final int FILE_BUFFER_SIZE = 2 * BUFFER_SIZE;

	/** The interval to sleep in case of error. */
	private static final int SLEEP_INTERVAL = 1000;

	/** Max error count. */
	private static final int ERROR_COUNT = 3;

	/** The quality of the bitmap when compress. */
	private static final int BITMAP_QUALITY = 100;

	/**
	 * Create a new DownloadTask singleton based on the current context.
	 * 
	 * @param currentContext
	 *            Current app context
	 */
	private DownloadTask(final Context currentContext) {
		this.context = currentContext;
		forceStop = false;
		lockThread = new Object();
		intent = new Intent(BaseKeys.BACKGROUND_DATA_FILTER);

		jobsLow = new Vector<Job>();
		jobsHigh = new Vector<Job>();

		downloadThread = new Thread(new DownloadThread(), "DownloadImagesTask");
		downloadThread.setPriority(Thread.MIN_PRIORITY);
		downloadThread.setDaemon(true);
		downloadThread.start();
	}

	/**
	 * Get the download task singleton based on current context.
	 * 
	 * @param currentContext
	 *            The current app {@link Context}
	 * @return A {@link DownloadTask} object
	 */
	public static DownloadTask getInstance(final Context currentContext) {
		if (sInstance == null) {
			sInstance = new DownloadTask(currentContext);
		}
		return sInstance;
	}

	/**
	 * Add new job to the back of the queue.
	 * 
	 * @param url
	 *            the url
	 * @param local
	 *            the local
	 * @param listener
	 *            the listener
	 * @param type
	 *            the type
	 * @param priority
	 *            the priority
	 */
	public void enqueue(final String url, final String local,
			final DownloadListener listener, final Type type,
			final Priority priority) {
		enqueue(url, local, listener, type, priority, null);
	}

	/**
	 * Add new job to the back of the queue.
	 * 
	 * @param url
	 *            The URL to the object on server
	 * @param local
	 *            The path to where the file should be saved after download
	 * @param listener
	 *            Listener that will be notified on fail and success
	 * @param type
	 *            {@link Type} of the object to be download
	 * @param priority
	 *            {@link Priority} of this job. Used to determine which of the
	 *            queues to use
	 * @param imageMaxSize
	 *            Image max size or null if don't care
	 */
	public void enqueue(final String url, final String local,
			final DownloadListener listener, final Type type,
			final Priority priority, final ImageMaxSize imageMaxSize) {

		Job job = new Job(url, local, type, imageMaxSize, listener);

		switch (priority) {
		case HighHead:
			jobsHigh.add(0, job);
			break;
		case HighTail:
			jobsHigh.add(job);
			break;
		case LowHead:
			jobsLow.add(0, job);
			break;
		case LowTail:
			jobsLow.add(job);
			break;
		default:
			break;
		}

		wakeUp();
	}

	/**
	 * Wake up the background thread and check for jobs.
	 */
	public void wakeUp() {
		forceStop = false;

		synchronized (lockThread) {
			needRefresh = true;
			lockThread.notify();
		}
	}

	/**
	 * Stop the background thread.
	 */
	public void stop() {
		synchronized (lockThread) {
			forceStop = true;
			lockThread.notifyAll();
		}
	}

	/**
	 * Thread that run in background and download things from server and save
	 * them into cache folder.
	 */
	private class DownloadThread implements Runnable {

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run() {
			while (!forceStop) {
				while (!forceStop && ((jobsLow.size() + jobsHigh.size()) > 0)) {

					Job job = null;
					boolean wasHigh = false;

					if (jobsHigh.size() > 0) {
						job = jobsHigh.remove(0);
						wasHigh = true;
						Ln.d("Take [%s] from HIGH!", job.getType().toString());
					}

					if (job == null) {
						job = jobsLow.remove(0);
						Ln.d("Take [%s] from LOW!", job.getType().toString());
					}

					if (job != null) {
						File localFile = new File(job.getLocalPath());

						boolean result = false;

						if (localFile.exists()) {
							if (job.getType() == Type.Image) {
								decode(job);
							} else {
								if (job.getListener() != null) {
									job.getListener().onSucces(null,
											localFile.getPath());
								}
							}
							result = true;
						} else {
							if (wasHigh
									|| Connectivity.isConnectedViaWifi(context)) {
								intent.putExtra(BaseKeys.IS_DOWNLOADING, true);
								context.sendBroadcast(intent);
								result = download(job);
								intent.putExtra(BaseKeys.IS_DOWNLOADING, false);
								context.sendBroadcast(intent);
							} else {
								Ln.i("Device is not connected via WiFi. "
										+ "We will download only High Priority data!");
								result = false;
							}
						}

						if (!result) {
							job.setErrorCount(job.getErrorCount() + 1);

							if (job.getErrorCount() <= ERROR_COUNT) {
								if (wasHigh) {
									jobsHigh.add(job);
								} else {
									jobsLow.add(job);
								}
							}
						}

					}
				}

				// Wait for notifications
				synchronized (lockThread) {
					if (!forceStop && !needRefresh) {
						try {
							Ln.i("Refresh end! Waiting...");
							lockThread.wait();
						} catch (InterruptedException e) {
							Ln.w(e);
						}
					}
				}
			}
		}
	}

	/**
	 * Download the data from server.
	 * 
	 * @param job
	 *            {@link Job} details
	 * @return true, if successful
	 */
	private boolean download(final Job job) {
		try {
			URL request = new URL(job.getRemote());

			InputStream is = (InputStream) request.getContent();

			boolean success = false;

			if (job.getLocalPath() != null) {

				boolean hasError = false;
				do {
					hasError = false;
					try {

						File localFile = new File(job.getLocalPath());

						if (localFile.getParentFile() != null) {
							if (!localFile.getParentFile().exists()) {
								Ln.e("Dir [%s] not exists. Create!",
										localFile.getParent());
								if (!localFile.getParentFile().mkdirs()) {
									throw new DirNotCreatedException();
								}
							}
						}

						OutputStream bfos = new BufferedOutputStream(
								new FileOutputStream(job.getLocalPath()),
								FILE_BUFFER_SIZE);

						try {
							Ln.d("Downloading [%s] from [%s] into [%s]", job
									.getType().toString(), job.getRemote(), job
									.getRemote());

							if (job.getType() == Type.Image) {

								if (job.getImageMaxSize() != null) {
									BitmapFactory.Options o = new BitmapFactory.Options();
									o.inJustDecodeBounds = true;

									BitmapFactory.decodeStream(is, null, o);

									int widthTmp = o.outWidth;
									int heightTmp = o.outHeight;

									Ln.i("Image size is: width=[%d] height=[%d]",
											widthTmp, heightTmp);

									int scale = 0;

									if (widthTmp > job.getImageMaxSize()
											.getWidth()
											|| heightTmp > job
													.getImageMaxSize()
													.getHeight()) {
										while (true) {
											if ((widthTmp / 2) < job
													.getImageMaxSize()
													.getWidth()
													|| (heightTmp / 2) < job
															.getImageMaxSize()
															.getHeight()) {
												break;
											}
											widthTmp /= 2;
											heightTmp /= 2;
											scale++;
										}
									}

									BitmapFactory.Options o2 = new BitmapFactory.Options();
									o2.inSampleSize = (int) Math.pow(2, scale);
									o2.inPurgeable = true;

									Ln.i("Will resize image with scale=[%d]",
											o2.inSampleSize);

									Bitmap bmp = BitmapFactory.decodeStream(is,
											null, o2);
									if (bmp != null) {
										bmp.compress(CompressFormat.PNG,
												BITMAP_QUALITY, bfos);
									}
								} else {
									Bitmap bmp = BitmapFactory.decodeStream(is);
									if (bmp != null) {
										bmp.compress(CompressFormat.PNG,
												BITMAP_QUALITY, bfos);
									}
								}

							} else if (job.getType() == Type.Text) {

								read(is, bfos);

							} else if (job.getType() == Type.Audio) {

								read(is, bfos);

							} else if (job.getType() == Type.Video) {

								read(is, bfos);

								if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.ECLAIR_MR1) {

									Bitmap bMap = VersionHelper.getThumb(job);

									if (bMap != null) {
										int extStart = job.getLocalPath()
												.lastIndexOf(".");

										String thumbPath = job.getLocalPath()
												.substring(0, extStart)
												+ ".png";

										OutputStream bthumbFos = new BufferedOutputStream(
												new FileOutputStream(thumbPath),
												FILE_BUFFER_SIZE);

										try {
											bMap.compress(CompressFormat.PNG,
													BITMAP_QUALITY, bthumbFos);
										} finally {
											bthumbFos.flush();
											bthumbFos.close();
										}
									}
								}
							}

							success = true;

						} catch (FileNotFoundException e) {
							throw e;
						} catch (IOException e) {
							Ln.e(e);
						} finally {
							is.close();
							bfos.flush();
							bfos.close();
						}

						if (success) {
							Bitmap bitmap = null;

							if (job.getType() == Type.Image) {
								bitmap = BitmapFactory.decodeFile(job
										.getLocalPath());
							}

							if (job.getListener() != null) {
								job.getListener().onSucces(bitmap,
										job.getLocalPath());
							}

							return true;
						} else {
							if (job.getListener() != null) {
								job.getListener().failed();
							}
						}
					} catch (DirNotCreatedException e) {
						Ln.e(e);
						hasError = true;

						try {
							Ln.e("Directory could not be created! Sleep for 1 sec!");
							Thread.sleep(SLEEP_INTERVAL);
						} catch (InterruptedException ex) {
							Ln.w(e);
						}

					} catch (FileNotFoundException e) {
						Ln.e(e);
						hasError = true;

						try {
							Ln.e("File not found! Sleep for 1 sec!");
							Thread.sleep(SLEEP_INTERVAL);
						} catch (InterruptedException ex) {
							Ln.w(ex);
						}

					}
				} while (hasError);
			}
		} catch (MalformedURLException e) {
			Ln.e(e);
		} catch (IOException e) {
			Ln.e(e);
		} catch (Exception e) {
			Ln.e(e);
		}

		return false;
	}

	/**
	 * Read from InputStream and writes to OutputStream.
	 * 
	 * @param is
	 *            The {@link InputStream}
	 * @param bfos
	 *            The {@link OutputStream}
	 */
	private void read(final InputStream is, final OutputStream bfos) {

		byte[] buffer = new byte[BUFFER_SIZE];
		int l = -1;

		try {
			while ((l = is.read(buffer)) != -1) {
				bfos.write(buffer, 0, l);
			}
		} catch (IOException e) {
			Ln.e(e);
		}
	}

	/**
	 * The image was found on local storage so just decode it in the background
	 * thread and return the {@link Bitmap} object.
	 * 
	 * @param job
	 *            {@link Job} details
	 */
	private void decode(final Job job) {
		if (job.getType() == Type.Image) {

			if (job.getListener() != null) {
				Bitmap bitmap = null;
				Ln.d("Decode file [%s]", job.getLocalPath());

				bitmap = BitmapFactory.decodeFile(job.getLocalPath());

				if (bitmap != null) {
					job.getListener().onSucces(bitmap, job.getLocalPath());
				} else {
					job.getListener().failed();
				}
			}
		}
	}

	/**
	 * Type of the object that will be downloaded.
	 * 
	 */
	public enum Type {
		/** The Image. */
		Image,
		/** The Video. */
		Video,
		/** The Text. */
		Text,
		/** The Audio. */
		Audio
	};

	/**
	 * Priority used for download. There are two queues. An object that is
	 * enqueued with HIGH priority will be procesed before the ones that are
	 * added with LOW.
	 */
	public enum Priority {
		/** The High head. */
		HighHead,
		/** The High tail. */
		HighTail,
		/** The Low head. */
		LowHead,
		/** The Low tail. */
		LowTail
	};

	/**
	 * The Class VersionHelper.
	 */
	static class VersionHelper {

		/**
		 * Gets the thumb.
		 * 
		 * @param job
		 *            the job
		 * @return the thumb
		 */
		static Bitmap getThumb(final Job job) {
			Bitmap bMap = ThumbnailUtils.createVideoThumbnail(
					job.getLocalPath(), MediaStore.Video.Thumbnails.MINI_KIND);

			return bMap;
		}
	}
}
